// Copyright (c) 2024 CHANGLEI. All rights reserved.

import 'dart:math';

import 'package:charts_grasp/src/chart/axis/axis.dart';
import 'package:charts_grasp/src/chart/axis/category_axis.dart';
import 'package:charts_grasp/src/chart/axis/datetime_axis.dart';
import 'package:charts_grasp/src/chart/axis/datetime_category_axis.dart';
import 'package:charts_grasp/src/chart/axis/logarithmic_axis.dart';
import 'package:charts_grasp/src/chart/axis/numeric_axis.dart';
import 'package:charts_grasp/src/chart/base/chart_base.dart';
import 'package:charts_grasp/src/chart/chart_series/xy_data_series.dart';
import 'package:charts_grasp/src/chart/common/cartesian_state_properties.dart';
import 'package:charts_grasp/src/chart/utils/enum.dart';
import 'package:charts_grasp/src/chart/utils/helper.dart';
import 'package:charts_grasp/src/common/utils/helper.dart';
import 'package:charts_grasp/src/core/core.dart';
import 'package:flutter/material.dart';

@immutable
class PlotBand {
  const PlotBand({
    this.isVisible = true,
    this.start,
    this.end,
    this.color = Colors.grey,
    this.opacity = 1.0,
    this.borderColor = Colors.transparent,
    this.borderWidth = 0,
    this.text,
    this.textStyle,
    this.isRepeatable = false,
    this.repeatEvery = 1,
    this.verticalTextPadding,
    this.horizontalTextPadding,
    this.repeatUntil,
    this.textAngle,
    this.shouldRenderAboveSeries = false,
    this.sizeType = DateTimeIntervalType.auto,
    this.dashArray = const <double>[0, 0],
    this.size,
    this.associatedAxisStart,
    this.associatedAxisEnd,
    this.verticalTextAlignment = TextAnchor.middle,
    this.horizontalTextAlignment = TextAnchor.middle,
    this.gradient,
  });

  final bool isVisible;

  final dynamic start;

  final dynamic end;

  final String? text;

  final TextStyle? textStyle;

  final Color color;

  final Color borderColor;

  final double borderWidth;

  final double opacity;

  final bool isRepeatable;

  final dynamic repeatEvery;

  final dynamic repeatUntil;

  final double? textAngle;

  final bool shouldRenderAboveSeries;

  final DateTimeIntervalType sizeType;

  final List<double> dashArray;

  final dynamic size;

  final dynamic associatedAxisStart;

  final dynamic associatedAxisEnd;

  final TextAnchor verticalTextAlignment;

  final TextAnchor horizontalTextAlignment;

  final LinearGradient? gradient;

  final String? verticalTextPadding;

  final String? horizontalTextPadding;

  @override
  bool operator ==(Object other) {
    if (identical(this, other)) {
      return true;
    }
    if (other.runtimeType != runtimeType) {
      return false;
    }

    return other is PlotBand &&
        other.isVisible == isVisible &&
        other.start == start &&
        other.end == end &&
        other.color == color &&
        other.opacity == opacity &&
        other.borderColor == borderColor &&
        other.borderWidth == borderWidth &&
        other.text == text &&
        other.textStyle == textStyle &&
        other.isRepeatable == isRepeatable &&
        other.repeatEvery == repeatEvery &&
        other.verticalTextPadding == verticalTextPadding &&
        other.horizontalTextPadding == horizontalTextPadding &&
        other.repeatUntil == repeatUntil &&
        other.textAngle == textAngle &&
        other.shouldRenderAboveSeries == shouldRenderAboveSeries &&
        other.sizeType == sizeType &&
        other.dashArray == dashArray &&
        other.size == size &&
        other.associatedAxisStart == associatedAxisStart &&
        other.associatedAxisEnd == associatedAxisEnd &&
        other.verticalTextAlignment == verticalTextAlignment &&
        other.horizontalTextAlignment == horizontalTextAlignment &&
        other.gradient == gradient;
  }

  @override
  int get hashCode {
    final List<Object?> values = <Object?>[
      isVisible,
      start,
      end,
      color,
      opacity,
      borderColor,
      borderWidth,
      text,
      textStyle,
      isRepeatable,
      repeatEvery,
      verticalTextPadding,
      horizontalTextPadding,
      repeatUntil,
      textAngle,
      shouldRenderAboveSeries,
      sizeType,
      dashArray,
      size,
      associatedAxisStart,
      associatedAxisEnd,
      verticalTextAlignment,
      horizontalTextAlignment,
      gradient,
    ];
    return Object.hashAll(values);
  }
}

CustomPainter getPlotBandPainter({
  required CartesianStateProperties stateProperties,
  required bool shouldRenderAboveSeries,
  required ValueNotifier<num> notifier,
}) {
  return _PlotBandPainter(
    stateProperties: stateProperties,
    shouldRenderAboveSeries: shouldRenderAboveSeries,
    notifier: notifier,
  );
}

class _PlotBandPainter extends CustomPainter {
  _PlotBandPainter({
    required this.stateProperties,
    required this.shouldRenderAboveSeries,
    required ValueNotifier<num> notifier,
  })  : chart = stateProperties.chart,
        super(repaint: notifier);

  final CartesianStateProperties stateProperties;

  final CgCartesianChart chart;

  final bool shouldRenderAboveSeries;

  @override
  void paint(Canvas canvas, Size size) {
    Rect clipRect;
    for (int axisIndex = 0; axisIndex < stateProperties.chartAxis.axisRenderersCollection.length; axisIndex++) {
      final ChartAxisRenderer axisRenderer = stateProperties.chartAxis.axisRenderersCollection[axisIndex];
      final ChartAxisRendererDetails axisDetails = AxisHelper.getAxisRendererDetails(axisRenderer);
      final ChartAxis axis = axisDetails.axis;
      for (int j = 0; j < axis.plotBands.length; j++) {
        final PlotBand plotBand = axis.plotBands[j];
        if (plotBand.isVisible && shouldRenderAboveSeries != plotBand.shouldRenderAboveSeries) {
          clipRect = Rect.fromLTRB(
              stateProperties.chartAxis.axisClipRect.left,
              stateProperties.chartAxis.axisClipRect.top,
              stateProperties.chartAxis.axisClipRect.right,
              stateProperties.chartAxis.axisClipRect.bottom,);
          canvas.clipRect(clipRect);
          _renderPlotBand(canvas, axisRenderer, plotBand);
        }
      }
    }
  }

  ChartLocation _getStartAndEndValues(ChartAxisRenderer axisRenderer,
      dynamic start,
      dynamic end,
      PlotBand plotBand,
      bool isNeedRepeat,) {
    final ChartAxisRendererDetails axisDetails = AxisHelper.getAxisRendererDetails(axisRenderer);
    dynamic startValue = start is String && num.tryParse(start) != null ? num.tryParse(start) : start;

    dynamic endValue = end is String && num.tryParse(end) != null ? num.tryParse(end) : end;
    if (axisRenderer is DateTimeAxisRenderer) {
      startValue = startValue is DateTime ? startValue.millisecondsSinceEpoch : startValue;
      endValue = isNeedRepeat
          ? plotBand.repeatUntil is DateTime
              ? plotBand.repeatUntil.millisecondsSinceEpoch
              : plotBand.repeatUntil
          : endValue is DateTime
              ? endValue.millisecondsSinceEpoch
              : endValue;
    } else if (axisDetails is CategoryAxisDetails) {
      startValue = startValue is num ? startValue : axisDetails.labels.indexOf(startValue);
      endValue = isNeedRepeat
          ? plotBand.repeatUntil is num
              ? plotBand.repeatUntil.floor()
              : axisDetails.labels.indexOf(plotBand.repeatUntil)
          : endValue is num
              ? endValue
              : axisDetails.labels.indexOf(endValue);
    }
    if (axisDetails is DateTimeCategoryAxisDetails) {
      startValue = startValue is num
          ? startValue
          : (startValue is DateTime
              ? axisDetails.labels.indexOf(axisDetails.axis.isVisible
                  ? axisDetails.dateFormat.format(startValue)
                  : startValue.microsecondsSinceEpoch.toString(),)
              : axisDetails.labels.indexOf(startValue));
      endValue = isNeedRepeat
          ? plotBand.repeatUntil is num
              ? plotBand.repeatUntil.floor()
              : axisDetails.labels.indexOf(plotBand.repeatUntil)
          : endValue is num
              ? endValue
              : endValue is DateTime
                  ? axisDetails.labels.indexOf(axisDetails.axis.isVisible
                      ? axisDetails.dateFormat.format(endValue)
                      : endValue.microsecondsSinceEpoch.toString(),)
                  : axisDetails.labels.indexOf(endValue);
    } else if (axisRenderer is LogarithmicAxisRenderer || axisRenderer is NumericAxisRenderer) {
      endValue = isNeedRepeat ? plotBand.repeatUntil : endValue;
    }
    return ChartLocation(startValue.toDouble(), endValue.toDouble());
  }

  void _renderPlotBand(Canvas canvas, ChartAxisRenderer axisRenderer, PlotBand plotBand) {
    num startValue;
    num endValue;
    final ChartAxisRendererDetails axisDetails = AxisHelper.getAxisRendererDetails(axisRenderer);
    final bool isNeedRepeat = plotBand.isRepeatable && plotBand.repeatUntil != null && plotBand.repeatEvery != null;

    final ChartLocation startAndEndValues = _getStartAndEndValues(
        axisRenderer,
        plotBand.start ?? axisDetails.visibleRange!.minimum,
        plotBand.end ?? axisDetails.visibleRange!.maximum,
        plotBand,
        isNeedRepeat,);
    startValue = startAndEndValues.x;
    endValue = startAndEndValues.y;
    if (isNeedRepeat) {
      num repeatStart = startValue;
      num repeatEnd;
      while (repeatStart < endValue) {
        repeatEnd = _getPlotBandValue(axisRenderer, plotBand, repeatStart, plotBand.size ?? plotBand.repeatEvery);
        repeatEnd = repeatEnd > endValue ? endValue : repeatEnd;
        _renderPlotBandElement(axisRenderer, repeatStart, repeatEnd, plotBand, canvas);
        repeatStart = plotBand.size != null
            ? _getPlotBandValue(axisRenderer, plotBand, repeatStart, plotBand.repeatEvery)
            : repeatEnd;
      }
    } else {
      _renderPlotBandElement(axisRenderer, startValue, endValue, plotBand, canvas);
    }
  }

  num _getPlotBandValue(ChartAxisRenderer axisRenderer, PlotBand plotBand, num value, num increment) {
    final int addValue = increment.toInt();
    final ChartAxisRendererDetails axisDetails = AxisHelper.getAxisRendererDetails(axisRenderer);
    DateTimeIntervalType intervalType;
    if (axisDetails is DateTimeAxisDetails) {
      intervalType =
          (plotBand.sizeType == DateTimeIntervalType.auto) ? axisDetails.actualIntervalType : plotBand.sizeType;
      DateTime date = DateTime.fromMillisecondsSinceEpoch(value.toInt());
      switch (intervalType) {
        case DateTimeIntervalType.years:
          date = DateTime(
              date.year + addValue, date.month, date.day, date.hour, date.minute, date.second, date.millisecond,);
          break;
        case DateTimeIntervalType.months:
          date = DateTime(
              date.year, date.month + addValue, date.day, date.hour, date.minute, date.second, date.millisecond,);
          break;
        case DateTimeIntervalType.days:
          date = DateTime(
              date.year, date.month, date.day + addValue, date.hour, date.minute, date.second, date.millisecond,);
          break;
        case DateTimeIntervalType.hours:
          date = DateTime(
              date.year, date.month, date.day, date.hour + addValue, date.minute, date.second, date.millisecond,);
          break;
        case DateTimeIntervalType.minutes:
          date = DateTime(
              date.year, date.month, date.day, date.hour, date.minute + addValue, date.second, date.millisecond,);
          break;
        case DateTimeIntervalType.seconds:
          date = DateTime(
              date.year, date.month, date.day, date.hour, date.minute, date.second + addValue, date.millisecond,);
          break;
        case DateTimeIntervalType.milliseconds:
          date = DateTime(
              date.year, date.month, date.day, date.hour, date.minute, date.second, date.millisecond + addValue,);
          break;
        case DateTimeIntervalType.auto:
          break;
      }
      value = date.millisecondsSinceEpoch;
    } else {
      value += addValue;
    }
    return value;
  }

  void _renderPlotBandElement(ChartAxisRenderer axisRenderer,
      num startValue,
      num endValue,
      PlotBand plotBand,
      Canvas canvas,) {
    ChartLocation startPoint;
    ChartLocation endPoint;
    ChartLocation segmentStartPoint;
    ChartLocation segmentEndPoint;
    Rect plotBandRect;
    int textAngle;
    double? left;
    double? top;
    double? bottom;
    double? right;
    final ChartAxisRendererDetails axisDetails = AxisHelper.getAxisRendererDetails(axisRenderer);
    final ChartAxis axis = axisDetails.axis;
    final Rect axisRect = calculatePlotOffset(
        stateProperties.chartAxis.axisClipRect,
        Offset(axisDetails.orientation == AxisOrientation.horizontal ? axis.plotOffset : 0,
            axisDetails.orientation == AxisOrientation.vertical ? axis.plotOffset : 0,),);
    final num visibleMin = axis is LogarithmicAxis
        ? pow(axis.logBase, axisDetails.visibleRange!.minimum)
        : axisDetails.visibleRange!.minimum;
    final num visibleMax = axis is LogarithmicAxis
        ? pow(axis.logBase, axisDetails.visibleRange!.maximum)
        : axisDetails.visibleRange!.maximum;

    endValue < 0
        ? endValue <= visibleMin
            ? endValue = visibleMin
            : endValue = endValue
        : endValue >= visibleMax
            ? endValue = visibleMax
            : endValue = endValue;

    startValue < 0
        ? startValue <= visibleMin
            ? startValue = visibleMin
            : startValue = startValue
        : startValue >= visibleMax
            ? startValue = visibleMax
            : startValue = startValue;

    startPoint = calculatePoint(
        startValue, startValue, axisDetails, axisDetails, stateProperties.requireInvertedAxis, null, axisRect,);
    endPoint = calculatePoint(
        endValue, endValue, axisDetails, axisDetails, stateProperties.requireInvertedAxis, null, axisRect,);

    ChartAxisRenderer? segmentAxisRenderer;
    if (plotBand.associatedAxisStart != null || plotBand.associatedAxisEnd != null) {
      if (axis.associatedAxisName == null) {
        segmentAxisRenderer = (axisDetails.orientation == AxisOrientation.horizontal)
            ? stateProperties.chartAxis.primaryYAxisRenderer
            : stateProperties.chartAxis.primaryXAxisRenderer;
      } else {
        for (int axisIndex = 0; axisIndex < stateProperties.chartAxis.axisRenderersCollection.length; axisIndex++) {
          final ChartAxisRenderer targetAxisRenderer = stateProperties.chartAxis.axisRenderersCollection[axisIndex];
          final ChartAxisRendererDetails targetAxisDetails = AxisHelper.getAxisRendererDetails(targetAxisRenderer);
          if (axis.associatedAxisName == targetAxisDetails.name) {
            segmentAxisRenderer = axisRenderer;
          }
        }
      }
      final ChartLocation startAndEndValues = _getStartAndEndValues(segmentAxisRenderer!,
          plotBand.associatedAxisStart ?? startValue, plotBand.associatedAxisEnd ?? endValue, plotBand, false,);
      final ChartAxisRendererDetails segmentAxisDetails = AxisHelper.getAxisRendererDetails(segmentAxisRenderer);
      if (segmentAxisDetails.orientation == AxisOrientation.horizontal) {
        segmentStartPoint = calculatePoint(startAndEndValues.x, startValue, segmentAxisDetails, axisDetails,
            stateProperties.requireInvertedAxis, null, axisRect,);
        segmentEndPoint = calculatePoint(startAndEndValues.y, endValue, segmentAxisDetails, axisDetails,
            stateProperties.requireInvertedAxis, null, axisRect,);
        left = plotBand.associatedAxisStart != null ? segmentStartPoint.x : axisRect.left;
        right = plotBand.associatedAxisEnd != null ? segmentEndPoint.x : axisRect.right;
      } else {
        segmentStartPoint = calculatePoint(startValue, startAndEndValues.x, axisDetails, segmentAxisDetails,
            stateProperties.requireInvertedAxis, null, axisRect,);
        segmentEndPoint = calculatePoint(endValue, startAndEndValues.y, axisDetails, segmentAxisDetails,
            stateProperties.requireInvertedAxis, null, axisRect,);
        top = plotBand.associatedAxisStart != null ? segmentStartPoint.y : axisRect.bottom;
        bottom = plotBand.associatedAxisEnd != null ? segmentEndPoint.y : axisRect.top;
      }
    }

    if (axisDetails.orientation == AxisOrientation.horizontal) {
      textAngle = plotBand.textAngle != null ? plotBand.textAngle!.toInt() : 270;
      plotBandRect =
          Rect.fromLTRB(left ?? startPoint.x, top ?? axisRect.top, right ?? endPoint.x, bottom ?? axisRect.bottom);
    } else {
      textAngle = plotBand.textAngle != null ? plotBand.textAngle!.toInt() : 0;
      plotBandRect =
          Rect.fromLTRB(left ?? axisRect.left, top ?? endPoint.y, right ?? axisRect.right, bottom ?? startPoint.y);
    }
    _drawPlotBand(plotBand, plotBandRect, textAngle, canvas, axis);
  }

  void _drawPlotBand(PlotBand plotBand, Rect plotBandRect, int textAngle, Canvas canvas, ChartAxis axis) {
    final List<double> dashArray = plotBand.dashArray;
    bool needDashLine = true;

    final TextStyle plotBandLabelStyle =
        stateProperties.renderingDetails.chartTheme.plotBandLabelTextStyle!.merge(plotBand.textStyle);
    Path? path;
    for (int i = 1; i < dashArray.length; i = i + 2) {
      if (dashArray[i] == 0) {
        needDashLine = false;
      }
    }

    path = Path()
      ..moveTo(plotBandRect.left, plotBandRect.top)
      ..lineTo(plotBandRect.left + plotBandRect.width, plotBandRect.top)
      ..lineTo(plotBandRect.left + plotBandRect.width, plotBandRect.top + plotBandRect.height)
      ..lineTo(plotBandRect.left, plotBandRect.top + plotBandRect.height)
      ..close();

    final Paint paint = Paint();
    Path? dashedPath;
    if (needDashLine) {
      paint.isAntiAlias = false;
      dashedPath = dashPath(path, dashArray: CircularIntervalList<double>(dashArray));
    } else {
      dashedPath = path;
    }
    Paint fillPaint;
    if (plotBand.gradient != null) {
      fillPaint = Paint()
        ..shader = plotBand.gradient!.createShader(plotBandRect)
        ..style = PaintingStyle.fill;
    } else {
      fillPaint = Paint()
        ..color = plotBand.color.withOpacity(plotBand.opacity)
        ..style = PaintingStyle.fill;
    }
    canvas.drawPath(path, fillPaint);
    if (plotBand.borderWidth > 0 && dashedPath != null) {
      canvas.drawPath(
          dashedPath,
          paint
            ..color = plotBand.borderColor.withOpacity(plotBand.opacity)
            ..style = PaintingStyle.stroke
            ..strokeWidth = plotBand.borderWidth,);
    }
    if (plotBand.text != null && plotBand.text!.isNotEmpty) {
      final Size textSize = measureText(plotBand.text!, plotBandLabelStyle, textAngle);
      num? x = 0;
      num? y = 0;
      if (plotBand.horizontalTextPadding != null && plotBand.horizontalTextPadding != '') {
        if (plotBand.horizontalTextPadding!.contains('%')) {
          x = percentageToValue(
              plotBand.horizontalTextPadding,
              chart.isTransposed
                  ? stateProperties.chartAxis.axisClipRect.bottom
                  : stateProperties.chartAxis.axisClipRect.right,);
        } else if (plotBand.horizontalTextPadding!.contains('px')) {
          x = double.parse(plotBand.horizontalTextPadding!.substring(0, plotBand.horizontalTextPadding!.length - 2));
        } else {
          x = double.parse(plotBand.horizontalTextPadding!);
        }
      }
      if (plotBand.verticalTextPadding != null && plotBand.verticalTextPadding != '') {
        if (plotBand.verticalTextPadding!.contains('%')) {
          y = percentageToValue(
              plotBand.verticalTextPadding,
              chart.isTransposed
                  ? stateProperties.chartAxis.axisClipRect.right
                  : stateProperties.chartAxis.axisClipRect.bottom,);
        } else if (plotBand.verticalTextPadding!.contains('px')) {
          y = double.parse(plotBand.verticalTextPadding!.substring(0, plotBand.verticalTextPadding!.length - 2));
        } else {
          y = double.parse(plotBand.verticalTextPadding!);
        }
      }

      drawText(
          canvas,
          plotBand.text!,
          Offset(
              plotBand.horizontalTextAlignment == TextAnchor.middle
                  ? (plotBandRect.left +
                          plotBandRect.width / 2 -
                          ((plotBandRect.left == stateProperties.chartAxis.axisClipRect.right)
                              ? 0
                              : (plotBandRect.right == stateProperties.chartAxis.axisClipRect.left)
                                  ? textSize.width
                                  : textSize.width / 2) +
                          x!) +
                      (textAngle != 0 ? textSize.width / 2 : 0)
                  : plotBand.horizontalTextAlignment == TextAnchor.start
                      ? plotBandRect.left + x!
                      : plotBandRect.right - textSize.width + x!,
              plotBand.verticalTextAlignment == TextAnchor.middle
                  ? (plotBandRect.top +
                          plotBandRect.height / 2 -
                          ((plotBandRect.bottom == stateProperties.chartAxis.axisClipRect.top)
                              ? textSize.height
                              : (plotBandRect.top == stateProperties.chartAxis.axisClipRect.bottom)
                                  ? 0 + y!
                                  : textSize.height + y!)) +
                      (textAngle != 0 ? textSize.height / 2 : 0)
                  : plotBand.verticalTextAlignment == TextAnchor.start
                      ? (plotBandRect.top - y!)
                      : plotBandRect.bottom - textSize.height - y!,),
          plotBandLabelStyle,
          textAngle,);
    }
  }

  @override
  bool shouldRepaint(_PlotBandPainter oldDelegate) => true;
}
